﻿using Nemerle;
using Nemerle.Assertions;
using Nemerle.Collections;
using Nemerle.Text;
using Nemerle.Utility;

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Nemerle.Imperative;
namespace Nemerle2.Compiler
{
  /// <summary>
  /// Описывает ветку (узел) дерева типов некоторого проекта.
  /// http://code.google.com/p/nemerle-2/wiki/TypesTreeNode
  /// </summary>
  public variant TypesTreeNode
  {
    | Type      { TypeInfo  : ITypeInfo; }
    | Ambiguous { Elems     : list[TypesTreeNode] }
    | MacroCall { Macro     : IMacroInfo; }
    | NamespaceReference

    public Project  : NProject;
    public Parent   : TypesTreeNode;
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public Name     : string;

    /// Don't use this field dirrectly! Use GetChildren() instead.
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private mutable _children       : Hashtable[string, TypesTreeNode];
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private static  _emptyChildren  : array[TypesTreeNode] = array(0);

    /// Root node ctor.
    internal this(project : NProject)
    {
      Project = project;
      Parent = null;
      Name    = "";
    }

    internal this([NotNull] parent : TypesTreeNode, [NotNull] name : string)
    {
      Parent = parent;
      Project = parent.Project;
      Name    = name;
    }

    public FullName : string
    {
      [Memoize]
      get
      {
        def loop(node : TypesTreeNode, acc) : list[string]
        {
          if (node.Parent == null) acc else loop(node.Parent, node.Name :: acc)
        }

        def name = loop(this, []);

        $<#..$(name; ".")#>
      }
    }

    public Count : int
    {
      get { if (HasChildren) _children.Count else 0 }
    }

    public override ToString() : string
    {
      match (this)
      {
        | Type(typeInfo)     =>
          if (typeInfo.DeclaringType == null)
            $"$(typeInfo.Name) (type node) Contains: $Count"
          else
            $"$(typeInfo.DeclaringType).$(typeInfo.Name) (Nested type node) Contains: $Count";

        | Ambiguous(elems)   => $"$Name (ambiguous between: (..$elems))"
        | MacroCall(m)       => $"$m (Macro)"
        | NamespaceReference => $"$Name (Namespace) Contains: $Count"
      }
    }

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public HasChildren : bool { get { _children != null } }

    /// This property mainly used for debuging purpose. Don't use it for loockup!
    [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
    public Children : array[TypesTreeNode]
    {
      get
      {
        if (HasChildren)
          _children.Values.OrderByDescending(_.IsNamespace).ThenBy(_.Name).NToArray()
        else
          _emptyChildren
      }
    }

    public Item[childrenName : string] : this
    {
      get { if (HasChildren) _children[childrenName] else null }
    }

    public TryGetItem(childrenName : string) : ValueOption[this]
    {
      if (HasChildren)
      {
        mutable value;

        if (_children.TryGetValue(childrenName, out value))
          ValueOption.Some(value)
        else
          ValueOption.None()
      }
      else
        ValueOption.None()
    }

    GetChildren() : Hashtable[string, TypesTreeNode]
    {
      unless (HasChildren)
        _children = Hashtable();

      _children
    }

    public HasChild(childName : string) : bool
    {
      HasChildren && _children.Contains(childName)
    }

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public IsNamespace : bool
    {
      get
      {
        match (this)
        {
          | NamespaceReference => true
          | Ambiguous(elems)   => elems.Exists(_ is TypesTreeNode.NamespaceReference)
          | _                  => false
        }
      }
    }

    #region Build tree

    MakeAmbiguous(newNode : TypesTreeNode, oldNode : TypesTreeNode) : TypesTreeNode
    {
      assert(newNode.Name == oldNode.Name);

      def ambiguousList =
        match (oldNode)
        {
          | Ambiguous(elems)  => newNode :: elems
          | _                 => [newNode, oldNode]
        };

      TypesTreeNode.Ambiguous(this, newNode.Name, ambiguousList)
    }

    internal AddChild([NotNull] newNode : TypesTreeNode) : TypesTreeNode
    {
      def hasChildren = HasChildren;
      def children    = GetChildren();
      def name        = newNode.Name;

      mutable node;

      if (hasChildren && children.TryGetValue(name, out node))
        node = MakeAmbiguous(newNode, node);
      else
        node = newNode;

      children[name] = node;
      node
    }

    internal AddChild([NotNull] typeInfo : ITypeInfo) : TypesTreeNode
    {
      AddChild(TypesTreeNode.Type(this, typeInfo.Name, typeInfo))
    }

    //internal AddChildMacroCall([NotNull] macro2 : IMacroInfo) : TypesTreeNode
    //{
    //  AddChild(TypesTreeNode.MacroCall(this, macro2.Name, macro2))
    //}

    TryAddChildNamespace([NotNull] name : string) : TypesTreeNode
    {
      def hasChildren = HasChildren;
      def children    = GetChildren();

      mutable node;

      if (hasChildren && children.TryGetValue(name, out node))
      { // if node with name "name" exists...
        match (node) // ...make ambiguous if it is not namespace
        {
          | MacroCall
          | Type
          | Ambiguous =>
            node           = MakeAmbiguous(TypesTreeNode.NamespaceReference(this, name), node);
            children[name] = node;

          | NamespaceReference => ()
        }
      }
      else
      {
        node           = TypesTreeNode.NamespaceReference(this, name);
        children[name] = node;
      }

      node
    }

    public AddFromReferencedAssembly(assembly : IReferencedAssembly) : void
    {
      def addTopLevelTypes(parentNode : TypesTreeNode, t : ITypeInfo, isFriend : bool) : void
      {
        def makePath(t : ITypeInfo, parentNode : TypesTreeNode, path : list[string]) : void
        {
          match (path)
          {
            | [_] | []     =>
              def current = parentNode.AddChild(t);
              // VladD2: TODO: Надо добавить более продвинутую систему фильрации заведомо недоступных типов.
              foreach (nested when t.IsExternallyAccessible(isFriend) in t.GetNestedTypes())
                makePath(nested, current, []);


            | head :: tail => makePath(t, parentNode.TryAddChildNamespace(head), tail)
          }
        }

        makePath(t, parentNode, t.FullName.SplitToList('.'))
      }
      
      def isFriend(assembly : IReferencedAssembly, to : IReferencedAssembly)
      {
        def isEqualsBytes(b1 : array[byte], b2 : array[byte]) : bool
        {
          def b1 = b1 ?? array[];
          def b2 = b2 ?? array[];
          b1.SequenceEqual(b2);
        }       
       
        def assemblyName = System.Reflection.AssemblyName(to.FullName);
        mutable result = false;
        foreach(attr when attr.FullName == Constants.InternalsVisibleToAttributeFullName in
          assembly.CustomAttributes.CustomAttributes)
        {
          def friendAssemblyName = System.Reflection.AssemblyName((attr.ArgumentValue[0] :> CustomAttributeValue.String).Value);
          when (friendAssemblyName.Name == to.Name
            && isEqualsBytes(friendAssemblyName.GetPublicKey(), assemblyName.GetPublicKey())
            && isEqualsBytes(friendAssemblyName.GetPublicKeyToken(), assemblyName.GetPublicKeyToken()))
            return true;
        }
        result;
       
      }
      def isFriend = isFriend(assembly, Project);
      // VladD2: TODO: Надо добавить более продвинутую систему фильрации заведомо недоступных типов.
      // _Eter_: TODO: подумать насчет того как убрать Cast()
      foreach (t when t.IsExternallyAccessible(isFriend) in assembly.TopLevelTypes.Cast.[NTypeInfo]())
        addTopLevelTypes(this, t, isFriend);
    }

    public RemoveTypesOfAssembly(_assembly : IReferencedAssembly) : void
    {
      throw NotImplementedException()
    }

    #endregion
  }
}
